/*
 * Copyright (c) Jipzingking 2016.
 */

package sdfs.namenode;


import com.sun.xml.internal.ws.developer.Serialization;
import sdfs.filetree.*;
import sdfs.protocol.INameNodeDataNodeProtocol;
import sdfs.protocol.INameNodeProtocol;
import sdfs.util.ProtoStuffUtil;
import sdfs.util.Tool;

import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.FileAlreadyExistsException;
import java.util.*;

@Serialization
public class NameNode implements INameNodeProtocol, INameNodeDataNodeProtocol ,Serializable{
    public  static final int NAME_NODE_PORT = 4340;
    private static final String DISK_FILENAME = "src/data/tree.node";
    public  final Map<UUID, FileNode> readonlyFile = new HashMap<>();
    public  final Map<UUID, FileNode> readwritePFile = new HashMap<>();
    public  DirNode root;
    private int blockNumber;

    public static NameNode getInstance(){
        NameNode nameNode=readObjectFromFile();
        if(nameNode==null){
            return new NameNode();
        }
        return nameNode;
    }

    public NameNode(){
        root=new DirNode();
        blockNumber=0;
        File file=new File(DISK_FILENAME);
        if (!file.exists())
            root=new DirNode();
        else{
            FileInputStream in;
            try{
                in=new FileInputStream(file);
                byte[] arr = new byte[in.available()];
                in.read(arr);
                root = (DirNode) ProtoStuffUtil.deserializeProtoStuffDataListToProductsObject(arr,DirNode.class);
                blockNumber=Integer.valueOf(root.getName());
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static NameNode readObjectFromFile(){
        NameNode nameNode=null;
        File file=new File(DISK_FILENAME);
        if(!file.exists()){
            return null;
        }
        FileInputStream in;
        try{
            in=new FileInputStream(file);
            byte[] arr = new byte[in.available()];
            in.read(arr);
            nameNode = (NameNode) ProtoStuffUtil.deserializeProtoStuffDataListToProductsObject(arr,NameNode.class);
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return nameNode;
    }

    public void close() throws IOException{
        File file=new File(DISK_FILENAME);
        FileOutputStream out;
        try{
            out=new FileOutputStream(file);
            //byte[] arr =  ProtoStuffUtil.serializeProtoStuffObject(this,this.getClass());
            root.setName(String.valueOf(blockNumber));
            byte[]arr = ProtoStuffUtil.serializeProtoStuffObject(root,root.getClass());//save root
            out.write(arr,0,arr.length);
            out.close();
           // Tool.printDir(this.root,"");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public SDFSFileChannel openReadonly(String fileUri) throws IOException {
        String dir = fileUri.substring(0,fileUri.lastIndexOf("/"));
        String fileName = fileUri.substring(fileUri.lastIndexOf("/"));

        if(!Tool.checkDir(dir,this.root))throw new FileNotFoundException(fileUri);

        DirNode dirNode = Tool.cdDir(dir,this.root);

        FileNode fileNode =null;
        for (Entry e: dirNode) {
            if(e.getName().equals(fileName) && e.getNode().getClass().equals(FileNode.class)){
                fileNode = (FileNode) e.getNode();
                break;
            }
        }
        if (fileNode==null)throw new FileNotFoundException();
        UUID uuid = UUID.randomUUID();
        readonlyFile.put(uuid,fileNode);

        return new SDFSFileChannel(this,uuid,fileNode.getFileSize(),fileNode.getBlockAmount(),fileNode,true);
    }

    @Override
    public SDFSFileChannel openReadwrite(String fileUri) throws IndexOutOfBoundsException, IllegalStateException, IOException {
        String dir = fileUri.substring(0,fileUri.lastIndexOf("/"));
        String fileName = fileUri.substring(fileUri.lastIndexOf("/")+1);

        if(!Tool.checkDir(dir,this.root))
            throw new FileNotFoundException(fileUri);

        DirNode dirNode = Tool.cdDir(dir,this.root);

        FileNode fileNode =null;
        for (Entry e: dirNode) {
            if(e.getName().equals(fileName) && e.getNode().getClass().equals(FileNode.class)){
                fileNode = (FileNode) e.getNode();
                break;
            }
        }
        if (fileNode==null)throw new FileNotFoundException();
        UUID uuid = UUID.randomUUID();
        readwritePFile.put(uuid,fileNode);
        //System.out.println(fileNode.getFileSize());
        return new SDFSFileChannel(this,uuid,fileNode.getFileSize(),fileNode.getBlockAmount(),fileNode,false);
    }

    @Override
    public SDFSFileChannel create(String fileUri) throws IOException {
        //for example sdfs://127.0.0.1:8888/test
        int start = fileUri.lastIndexOf("/");
        // get a.txt
        String fileName = fileUri.substring(start + 1);
        //get the specific dir entryList, such as sdfs://127.0.0.1:8888/foo
        DirNode dirNode = Tool.cdDir(fileUri.substring(0,start),this.root);
        //check if the file  exist
        for (Entry entry : dirNode) {
            if (entry.getNode().getClass().equals(FileNode.class)
                    && entry.getName().equals(fileName)) {
                throw new FileAlreadyExistsException(fileName);
            }
        }
        //create and add a.txt to  sdfs://127.0.0.1:8888/foo
        FileNode fileNode = new FileNode();
        fileNode.setName(fileName);
        dirNode.addEntry(new Entry(fileName,fileNode));

        UUID uuid = UUID.randomUUID();//add to map when open or create
        readwritePFile.put(uuid,fileNode);
        return new SDFSFileChannel(this,uuid,0,1,fileNode,false);
    }

    // close a file and then write the metadata to the dist
    @Override
    public void closeReadonlyFile(UUID fileUuid) throws IllegalStateException, IOException {
        readonlyFile.remove(fileUuid);
    }

    @Override
    public void closeReadwriteFile(UUID fileUuid, int newFileSize) throws IllegalStateException, IllegalArgumentException, IOException {
        readwritePFile.remove(fileUuid);
    }

    @Override
    public void mkdir(String fileUri) throws IOException {
        Tool.cdDir(fileUri,this.root);
    }

    @Override
    public LocatedBlock addBlock(UUID fileUuid) {
        FileNode fileNode=null ;
        if (readwritePFile.containsKey(fileUuid))fileNode = readwritePFile.get(fileUuid);
        if (readonlyFile.containsKey(fileUuid))throw new IllegalStateException();
        if (fileNode==null) throw new NullPointerException("file "+fileUuid+" is closed");

         LocatedBlock block;
         block = new LocatedBlock(++blockNumber);
         if(fileNode.getMainBlockInfo()==null){
             BlockInfo blocks = new BlockInfo();
             blocks.addLocatedBlock(block);
             fileNode.addBlockInfo(blocks);
         }
         else fileNode.getMainBlockInfo().addLocatedBlock(block);
        return block;
    }

    @Override
    public List<LocatedBlock> addBlocks(UUID fileUuid, int blockAmount) {
        FileNode fileNode=null ;
        if (readwritePFile.containsKey(fileUuid))fileNode = readwritePFile.get(fileUuid);
        if (readonlyFile.containsKey(fileUuid))throw new IllegalStateException();
        if (fileNode==null) throw new NullPointerException("file "+fileUuid+" is closed");

        LocatedBlock block;
        List<LocatedBlock> blockList = new ArrayList<>();
        for (int i = 0; i < blockAmount; i++) {
            //add size
            fileNode.setFileSize(fileNode.getFileSize() + 128*1024);
            try {
                block = new LocatedBlock(InetAddress.getLocalHost(),++blockNumber);
                if(null == fileNode.getMainBlockInfo()){
                    BlockInfo blocks = new BlockInfo();
                    blocks.addLocatedBlock(block);
                    fileNode.addBlockInfo(blocks);
                    blockList.add(block);
                }
                else {
                    fileNode.getMainBlockInfo().addLocatedBlock(block);
                    blockList.add(block);
                }

            } catch (UnknownHostException e) {
                e.printStackTrace();
            }

        }
        return blockList;
    }

    @Override
    public void removeLastBlock(UUID fileUuid) throws IllegalStateException {
        FileNode fileNode = null;
        if (readonlyFile.containsKey(fileUuid))fileNode = readonlyFile.get(fileUuid);
        if (readwritePFile.containsKey(fileUuid))fileNode = readwritePFile.get(fileUuid);
        if (fileNode==null) throw new NullPointerException("file "+fileUuid+" is closed");

        BlockInfo blocks = fileNode.getMainBlockInfo();
        if (blocks.getSize()==0) throw new IllegalStateException("no block in this file");
        blocks.removeLocatedBlock(blocks.getLastLocatedBlock());

    }

    @Override
    public void removeLastBlocks(UUID fileUuid, int blockAmount) throws IllegalStateException {
        FileNode fileNode = null;
        if (readonlyFile.containsKey(fileUuid))fileNode = readonlyFile.get(fileUuid);
        if (readwritePFile.containsKey(fileUuid))fileNode = readwritePFile.get(fileUuid);
        if (fileNode==null) throw new NullPointerException("file "+fileUuid+" is closed");

        BlockInfo blocks = fileNode.getMainBlockInfo();
        if (blocks.getSize()<blockAmount) throw new IllegalStateException("no block in this file");
        for (int i = 0; i < blockAmount; i++) {
            blocks.removeLocatedBlock(blocks.getLastLocatedBlock());
        }
    }




}
